package schedulo.core.task;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.text.ParseException;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.time.DateUtils;

import schedulo.core.task.provider.TaskProvider;

public class Task implements Cloneable {
	public class Log {
		private Calendar startDate, stopDate;

		public Calendar getStartDate() {
			return startDate;
		}

		public Calendar getStopDate() {
			return stopDate;
		}

		public void setStartDate(Calendar startDate) {
			this.startDate = startDate;
		}

		public void setStopDate(Calendar stopDate) {
			this.stopDate = stopDate;
		}
	}

	private static final int EXPREG_GROUP_DESCRIPTION = 10;

	private static final int EXPREG_GROUP_DUEDATE = 6;

	private static final int EXPREG_GROUP_FROMDATE = 5;

	private static final int EXPREG_GROUP_LOAD = 3;

	private static final int EXPREG_GROUP_LOADUNIT = 4;

	private static final int EXPREG_GROUP_LOGS = 8;

	private static final int EXPREG_GROUP_PRIORITY = 1;

	private static final int EXPREG_GROUP_REPEATABILITY = 7;

	private static final int EXPREG_GROUP_RESOURCES = 9;

	private static final int EXPREG_GROUP_STATUS = 2;

	private static final String[] expregs = new String[] {
	// priority (group 1) status (group 2)
			".*?([\\*-]?)\\[([xX/ ]?|(?:(?:[1-9][0-9]?|(?:100))%))\\](?:[\\*-])?\\s*",

			// load (group3) load unit (group 4)
			"(?:\\(\\s*(\\d(?:\\.\\d)?(?:\\-\\d(?:\\.\\d)?)?|\\?(?:\\.\\?)?)\\s*([mwsjdh])\\s*\\))?\\s*",

			// from (group 5) /due (group 6) date and repeatability (group
			// 7)
			"(?:\\(\\s*((?:CW)?[\\d \\-/]+)?\\s*->\\s*((?:CW)?[\\d \\-/]+)\\s*(none|hourly|daily|weekly|monthly|yearly)?\\s*\\))?\\s*",

			// logs (group 8)
			"(?:\\{\\s*((?:\\d{1,2}/\\d{1,2}/\\d{4}-\\d{1,2}:\\d{1,2}:\\d{1,2}\\s*>\\s*\\d{1,2}/\\d{1,2}/\\d{4}-\\d{1,2}:\\d{1,2}:\\d{1,2}\\s*;\\s*)+)\\s*\\})?\\s*",

			// resources (group 9)
			"((?:\\w{3}|[\\?]{1,3})(?:/\\w{3})*)?\\s*",

			// task description delimiter (semicolon or space)
			":?\\s+",

			// task description (group 10)
			"(.*)\\s*", };

	private static Pattern[] incrementalPatterns;

	public static final String LOAD_UNIT_DAY = "d";

	public static final String LOAD_UNIT_DAY2 = "j";

	public static final String LOAD_UNIT_HOUR = "h";

	public static final String LOAD_UNIT_MONTH = "m";

	public static final String LOAD_UNIT_UNKNOWN = "u";

	public static final String LOAD_UNIT_WEEK = "w";

	public static final String LOAD_UNIT_WEEK2 = "s";

	public static final String[] LOAD_UNITS = { LOAD_UNIT_HOUR, LOAD_UNIT_DAY2,
			LOAD_UNIT_WEEK2, LOAD_UNIT_MONTH, };

	private static Pattern[] patterns;

	public static final int PRIORITY_IMPORTANT = 1;

	public static final int PRIORITY_NORMAL = 0;

	public static final int PRIORITY_URGENT = 2;

	public static final String REPEAT_DAILY = "daily";

	public static final String REPEAT_HOURLY = "hourly";

	public static final String REPEAT_MONTHLY = "monthly";

	public static final String REPEAT_NONE = "none";

	public static final String REPEAT_WEEKLY = "weekly";

	public static final String REPEAT_YEARLY = "yearly";

	public static final int STATUS_DONE = 3;

	public static final int STATUS_NOT_STARTED = 0;

	public static final int STATUS_STARTED = 1;

	static {
		// prepare parser patterns

		// build patterns
		patterns = new Pattern[expregs.length];
		for (int i = 0; i < expregs.length; i++) {
			patterns[i] = Pattern.compile(expregs[i]);
		}

		// build incremental patterns (easier error reporting)
		incrementalPatterns = new Pattern[expregs.length];
		String current = "";
		for (int i = 0; i < expregs.length; i++) {
			current += expregs[i];
			incrementalPatterns[i] = Pattern.compile(current);
		}
	}

	/**
	 * Parse a TaskFile from an input stream decoding
	 * 
	 * @param in
	 * @return a list of warnings and errors encountered when parsed
	 * @throws IOException
	 */
	public static LinkedList<ParseMessage> load(InputStream in,
			Collection<Task> tasks) throws IOException {
		if (in == null) {
			throw new IOException("Invalid input stream");
		}

		if (tasks == null) {
			throw new IOException("Invalid tasks collection");
		}

		LinkedList<ParseMessage> parseMessages = new LinkedList<ParseMessage>();

		BufferedReader lineReader = new BufferedReader(
				new InputStreamReader(in));

		int lineCount = 0;
		String line;
		// for each line of the input stream
		while ((line = lineReader.readLine()) != null) {
			lineCount++;
			line = line.trim();

			if (line.length() == 0) {
				continue;
			}

			Task t = new Task();
			LinkedList<ParseMessage> parseMessagesLine = t.fromString(line);

			// only add task if no parse message or if no parse message in error
			if (parseMessagesLine == null || parseMessagesLine.size() == 0) {
				tasks.add(t);
			} else {
				// add task parse messages to global file parse messages
				parseMessages.addAll(parseMessagesLine);

				Iterator<ParseMessage> itPM = parseMessagesLine.iterator();
				boolean error = false;
				while (itPM.hasNext()) {
					if (itPM.next().severity == ParseMessage.SEVERITY_ERROR) {
						error = true;
					}
				}
				// add the task if no fatal parse error
				if (!error) {
					tasks.add(t);
				}
			}
		}
		return parseMessages;
	}

	public static void save(Collection<Task> tasks, OutputStream out)
			throws IOException {
		if (out == null) {
			throw new IOException("Invalid output stream");
		}

		// no task to print
		if (tasks == null) {
			throw new IOException("Invalid tasks collection");
		}

		Iterator<Task> itT = tasks.iterator();
		while (itT.hasNext()) {
			out.write((itT.next().toString() + "\n").getBytes());
		}
		out.flush();
	}

	private String description;

	private double loadMin, loadMax;

	private String loadUnit;

	private LinkedList<Log> logs;

	private int priority;

	private double progress;

	private String repeatability;

	private LinkedList<String> resources;

	// for todos, the start is the date the todo has been entered
	private String startDate, dueDate;

	private int status;

	private TaskProvider taskProvider;

	public Task() {
		reset();
	}

	public void addTimeTrack(Log log) {
		logs.add(log);
	}

	public boolean equals(Object obj) {
		if (obj instanceof Task) {
			Task task = (Task) obj;
			// TODO levenstein distance with distance parametrized via
			// configuration
			if (task.description != null && description != null) {
				return description.equalsIgnoreCase(task.description);
			}
		}
		return super.equals(obj);
	}

	public LinkedList<ParseMessage> fromString(String line) {

		reset();

		LinkedList<ParseMessage> parseMessages = new LinkedList<ParseMessage>();

		// parse the line with incremental pattern, and report error if any
		int lastMatchCharacter = 0;
		for (int i = 0; i < incrementalPatterns.length; i++) {
			Matcher matcher = incrementalPatterns[i].matcher(line);

			if (matcher.lookingAt()) {
				lastMatchCharacter = matcher.end();
			}
			if (!matcher.lookingAt()) {
				ParseMessage msg = new ParseMessage();
				// msg.lineNumber = lineCount;
				msg.severity = ParseMessage.SEVERITY_ERROR;
				msg.message = "Invalid token parsed";
				msg.line = line;
				msg.errorAtCharacter = lastMatchCharacter;
				parseMessages.add(msg);
				break;
			}

			// if the last incrementalPattern and success, then its now time
			// to group and create the task
			else if (i == incrementalPatterns.length - 1) {
				// cut the string in groups and fill a new task checking
				// values, and add the new task to the tasklist
				for (int j = 1; j <= matcher.groupCount(); j++) {
					System.out.print("(" + j + ":" + matcher.group(j) + ") ");
				}
				System.out.println("");

				String priority = matcher.group(EXPREG_GROUP_PRIORITY);
				setPriority(PRIORITY_NORMAL);
				if (priority != null) {
					if (priority.equals("*")) {
						setPriority(PRIORITY_URGENT);
					} else if (priority.equals("-")) {
						setPriority(PRIORITY_IMPORTANT);
					}
				}
				String status = matcher.group(EXPREG_GROUP_STATUS);
				if (status == null) {
					ParseMessage msg = new ParseMessage();
					// msg.lineNumber = lineCount;
					msg.severity = ParseMessage.SEVERITY_ERROR;
					msg.message = "Cannot parse task's status";
					msg.line = line;
					parseMessages.add(msg);
					break;
				} else {
					status = status.toLowerCase();
					if (status.equals("100%") || status.equals("x")) {
						setProgress(1.0);
					} else if (status.equals("/")) {
						setProgress(0.5);
					} else if (status.endsWith("%")) {
						double progress = Double.parseDouble(status.substring(
								0, status.lastIndexOf("%")));
						setProgress(progress / 100);
					}
				}

				String load = matcher.group(EXPREG_GROUP_LOAD);
				String loadUnit = matcher.group(EXPREG_GROUP_LOADUNIT);
				if (load != null) {

					// process the load value
					if (load.contains("?")) {
						setLoadUnit(LOAD_UNIT_UNKNOWN);
						setLoadMin(-1);
						setLoadMax(-1);
					} else if (load.contains("-")) {
						StringTokenizer tok = new StringTokenizer(load, "-");
						String loadMin = tok.nextToken();
						String loadMax = tok.nextToken();
						double loadMinValue = Double.parseDouble(loadMin);
						double loadMaxValue = Double.parseDouble(loadMax);

						// invert values if wrongly ordered
						if (loadMinValue > loadMaxValue) {
							double tmp = loadMinValue;
							loadMinValue = loadMaxValue;
							loadMaxValue = tmp;
						}
						setLoadMin(loadMinValue);
						setLoadMax(loadMaxValue);
					} else {
						double loadValue = Double.parseDouble(load);
						setLoadMin(loadValue);
						setLoadMax(loadValue);
					}

					// process the load unit
					if (loadUnit != null) {
						if (loadUnit.equals(LOAD_UNIT_DAY)) {
							setLoadUnit(LOAD_UNIT_DAY2);
						} else if (loadUnit.equals(LOAD_UNIT_WEEK)) {
							setLoadUnit(LOAD_UNIT_WEEK2);
						} else {
							setLoadUnit(loadUnit);
						}
					}
				}

				String startDate = matcher.group(EXPREG_GROUP_FROMDATE);
				if (startDate != null) {
					startDate = startDate.trim();
				}
				setStartDate(startDate);

				String dueDate = matcher.group(EXPREG_GROUP_DUEDATE);
				if (dueDate != null) {
					dueDate = dueDate.trim();
				}
				setDueDate(dueDate);

				String repeat = matcher.group(EXPREG_GROUP_REPEATABILITY);
				setRepeatability(REPEAT_NONE);
				if (repeat != null) {
					setRepeatability(repeat);
				}

				String logsString = matcher.group(EXPREG_GROUP_LOGS);
				if (logsString != null) {
					StringTokenizer st = new StringTokenizer(logsString, ";");
					while (st.hasMoreTokens()) {
						StringTokenizer st2 = new StringTokenizer(st
								.nextToken(), ">");
						Log log = new Log();
						log.setStartDate(parseDate(st2.nextToken()));
						log.setStopDate(parseDate(st2.nextToken()));
						logs.add(log);
					}
				}

				String resources = matcher.group(EXPREG_GROUP_RESOURCES);
				String description = matcher.group(EXPREG_GROUP_DESCRIPTION);

				// if description is less or equal than 3 characters, then shift
				// resource to description
				if (resources != null && description == null) {
					description = resources;
					resources = null;
				}

				setResources(resources);

				setDescription(description);
			}
		}
		return parseMessages;
	}

	public String getDescription() {
		return description;
	}

	public String getDueDate() {
		return dueDate;
	}

	public Calendar getDueDateDecoded() {
		if (dueDate != null) {
			return parseTaskDate(dueDate, false);
		}
		return null;
	}

	public double getLoadMax() {
		return loadMax;
	}

	public double getLoadMin() {
		return loadMin;
	}

	public String getLoadUnit() {
		return loadUnit;
	}

	public LinkedList<Log> getLogs() {
		return logs;
	}

	public int getPriority() {
		return priority;
	}

	public double getProgress() {
		return progress;
	}

	public String getRepeatability() {
		return repeatability;
	}

	public LinkedList<String> getResources() {
		return resources;
	}

	public String getStartDate() {
		return startDate;
	}

	public Calendar getStartDateDecoded() {
		if (startDate != null) {
			return parseTaskDate(startDate, true);
		}
		return null;
	}

	public int getStatus() {
		return status;
	}

	public TaskProvider getTaskProvider() {
		return taskProvider;
	}

	/**
	 * return true if startDate is >= date
	 * 
	 * @param date
	 * @param secondDelay
	 * @return
	 */
	public boolean needReminding(Calendar date) {
		return getStatus() != STATUS_DONE && date.after(getStartDateDecoded());
	}

	/**
	 * D[D]/M[M]/YYYY-h[h]:m[m]:s[s] -> Calendar
	 */
	private Calendar parseDate(String dateStr) {
		Date date;
		try {
			date = DateUtils.parseDate(dateStr, new String[] {
					"dd/MM/yyyy-HH:mm:ss", "d/M/y-H:m:s", "dd/MM/yyyy",
					"yyyy/MM/dd", "dd/MM" });

			Calendar c = Calendar.getInstance();
			c.clear();
			c.setTime(date);
			return c;
		} catch (ParseException e) {
			e.printStackTrace();
			return null;
		}

		// StringTokenizer st = new StringTokenizer(date);
		// String day = st.nextToken("/");
		// c.set(Calendar.DAY_OF_MONTH, Integer.parseInt(day));
		// String month = st.nextToken("/");
		//
		// // restablish the 0-11 instead of 1-12
		// c.set(Calendar.MONTH, Integer.parseInt(month) - 1);
		//
		// // nextToken("-") -> /YYYY, we need to cut the first char
		// String year = st.nextToken("-").substring(1);
		// c.set(Calendar.YEAR, Integer.parseInt(year));
		//
		// // nextToken(":") -> -hh, we need to cut the first char
		// String hour = st.nextToken(":").substring(1);
		// c.set(Calendar.HOUR, Integer.parseInt(hour));
		// String minute = st.nextToken(":");
		// c.set(Calendar.MINUTE, Integer.parseInt(minute));
		// String second = st.nextToken();
		// c.set(Calendar.SECOND, Integer.parseInt(second));
		// return c;
	}

	/**
	 * return the start/due date formatted in a calendar,
	 * 
	 * @param dateStr
	 * @param start
	 *            startDate == true, dueDate == false
	 * @return
	 */
	private Calendar parseTaskDate(String dateStr, boolean start) {
		Calendar c = Calendar.getInstance();
		if (dateStr.toUpperCase().startsWith("CW")) {
			int weekNum = Integer.parseInt(dateStr.toUpperCase().substring(2));
			int year = c.get(Calendar.YEAR);
			c.clear();
			c.set(Calendar.YEAR, year);
			c.set(Calendar.WEEK_OF_YEAR, weekNum);
			if (!start) {
				c.add(Calendar.DAY_OF_YEAR, 7);
			}
			return c;
		} else {
			return parseDate(dateStr);
		}
	}

	private String printDate(Calendar date, boolean precise) {
		String str = "";
		if (date == null) {
			return str;
		}

		// restore 1-12 instead of 0-11
		str += StringUtils
				.leftPad(date.get(Calendar.DAY_OF_MONTH) + "", 2, "0")
				+ "/"
				+ StringUtils.leftPad((date.get(Calendar.MONTH) + 1) + "", 2,
						"0")
				+ "/"
				+ StringUtils.leftPad(date.get(Calendar.YEAR) + "", 4, "0");

		if (precise) {
			str += "-"
					+ StringUtils.leftPad(date.get(Calendar.HOUR_OF_DAY) + "",
							2, "0")
					+ ":"
					+ StringUtils.leftPad(date.get(Calendar.MINUTE) + "", 2,
							"0")
					+ ":"
					+ StringUtils.leftPad(date.get(Calendar.SECOND) + "", 2,
							"0");
		}
		
		return str;
	}

	private void reset() {
		setDescription(null);
		setDueDate("");
		setLoadMax(-1);
		setLoadMin(-1);
		setLoadUnit(LOAD_UNIT_UNKNOWN);
		setLogs(new LinkedList<Log>());
		setPriority(PRIORITY_NORMAL);
		setProgress(0);
		setRepeatability(REPEAT_NONE);
		setResources(new LinkedList<String>());
		setStartDate("");
		setStatus(STATUS_NOT_STARTED);
	}

	public void setDescription(String description) {
		this.description = description;
	}

	public void setDueDate(String dueDate) {
		this.dueDate = dueDate;
	}

	public void setDueDate(Calendar c) {
		setDueDate(printDate(c,false));
	}

	public void setLoadMax(double loadMax) {
		this.loadMax = loadMax;
	}

	public void setLoadMin(double loadMin) {
		this.loadMin = loadMin;
	}

	public void setLoadUnit(String loadUnit) {
		this.loadUnit = loadUnit;
	}

	public void setLogs(LinkedList<Log> logs) {
		this.logs = logs;
	}

	public void setPriority(int priority) {
		this.priority = priority;
	}

	public void setProgress(double progress) {
		this.progress = progress;
		if (progress == 0.0) {
			setStatus(STATUS_NOT_STARTED);
		} else if (progress > 0.0 && progress < 1.0) {
			setStatus(STATUS_STARTED);
		} else if (progress == 1.0) {
			setStatus(STATUS_DONE);
		}
	}

	public void setRepeatability(String repeatability) {
		this.repeatability = repeatability;
	}

	public void setResources(LinkedList<String> resources) {
		this.resources = resources;
	}

	public void setStartDate(String startDate) {
		this.startDate = startDate;
	}
	
	public void setStartDate(Calendar c) {
		setStartDate(printDate(c, false));
	}

	public void setStatus(int status) {
		this.status = status;
	}

	public void setTaskProvider(TaskProvider taskProvider) {
		this.taskProvider = taskProvider;
	}

	public String toString() {
		String task = "";
		String priorityStr = "";
		switch (priority) {
		case PRIORITY_NORMAL:
			priorityStr = "";
			break;
		case PRIORITY_IMPORTANT:
			priorityStr = "*";
			break;
		case PRIORITY_URGENT:
			priorityStr = "-";
			break;
		}

		String statusStr = " ";
		switch (status) {
		case STATUS_NOT_STARTED:
			statusStr = " ";
			break;
		case STATUS_STARTED:
			if (progress != 0.0) {
				statusStr = (int) (progress * 100) + "%";
			} else {
				statusStr = "/";
			}
			break;
		case STATUS_DONE:
			statusStr = "X";
			break;
		}

		String loadStr = "";
		if ((loadMin >= 0 && loadMax >= 0) && loadUnit != null
				&& !loadUnit.equals(LOAD_UNIT_UNKNOWN)) {
			loadStr = " (" + loadMin;

			if (loadMin != loadMax) {
				loadStr += "-" + loadMax;
			}

			loadStr += loadUnit;

			loadStr += ") ";
		} else {
			loadStr = " ";
		}

		String dateStr = "";
		if (startDate != null || dueDate != null) {
			dateStr = "(";
			if (startDate != null) {
				dateStr += startDate;
			}
			if (dueDate != null) {
				dateStr += " -> " + dueDate;
			}
			if (repeatability != null && !repeatability.equals(REPEAT_NONE)) {
				dateStr += " " + repeatability;
			}
			dateStr += ") ";
		}

		String logsStr = "";
		if (logs != null && logs.size() > 0) {
			logsStr += "{";
			Iterator<Log> itLog = logs.iterator();
			while (itLog.hasNext()) {
				Log log = itLog.next();
				logsStr += printDate(log.startDate,true) + ">"
						+ printDate(log.stopDate,true) + ";";
			}
			logsStr += "} ";
		}

		String resourcesStr = "";
		if (resources != null && resources.size() > 0) {
			Iterator<String> it = resources.iterator();
			while (it.hasNext()) {
				resourcesStr += it.next();
				if (it.hasNext()) {
					resourcesStr += "/";
				}
			}
		}

		// only add ": " before description if exists optional task variables
		String optional = loadStr + dateStr + logsStr + resourcesStr;
		if (!optional.trim().equals("")) {
			optional += ": ";
		}

		task = priorityStr + "[" + statusStr + "]" + priorityStr + optional
				+ (description != null ? description : " ");
		return task;
	}

	private LinkedList<String> parseRessources(String resources) {
		LinkedList<String> resourcesList = new LinkedList<String>();
		if (resources != null) {
			// three delimiters
			StringTokenizer tok = new StringTokenizer(resources, "/, ");
			while (tok.hasMoreTokens()) {
				resourcesList.add(tok.nextToken());
			}
		}	
		return resourcesList;
	}
	
	public void setResources(String resources) {
		setResources(parseRessources(resources));
	}
}
